<?php
/**
 * Contains the Log_XML class.
 *
 * PHP version 5
 *
 * @category Logging
 * @package  Log
 * @author   Michael "Vladimyr" Gower <MichaelPeterGower@gmail.com>
 * @license  http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
 * @link     http://pear.php.net/pepr/pepr-proposal-show.php?id=487
 */

restore_include_path();

/**
 * Include the base class.
 */
require_once 'Log.php';

/**
 * Include a class to encapsulate additionally needed file functions.
 */
require_once 'XML/FileToolkit.php';

/**
 * Include Log_XML_Exception().
 */
require_once 'XML/Exception.php';

/**
 * The Log_XML class is an abstract implementation of the Log abstract
 * class that logs messages to an XML file.  See the Log_XML_Xtj class
 * or the Log_XML_Simple class for a concrete implimentation of Log_XML.
 *
 * If you want to delete the log file, be sure to use the autodelete option.
 * If the log is deleted while in use, it will simply be rewritten.
 *
 * Note: Assumes that both '/' and '\' are directory separators in quoted
 * literal strings, so stay away from 'nix filenames like 'my\file'.
 *
 * Based upon Log_file by Jon Parise <jon@php.net> and
 * Roman Neuhauser <neuhauser@bellavista.cz>.
 *
 * @category Logging
 * @package  Log
 * @author   Michael Peter "Vladimyr" Gower <designatevoid@gmail.com>
 * @license  http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
 * @link     http://pear.php.net/pepr/pepr-proposal-show.php?id=487
 * @since    Log_XML 1.0
 * @todo do exceptions for error handling for v2.
 * @todo make fully E_STRICT compatable by using Log2 (when Log2 developed)
 *
 * @example ../docs/examples/index.php    Using the xml handler.
 */
abstract class Log_XML extends Log
{
    /**
     * Constructs a new Log_XML object.
     *
     * Note: the autodelete argument will delete the log once.
     *
     * Postcondition: If directories need to be created to open the log,
     * they'll be created.
     *
     * For $conf we support the following parameters:
     * Parameter - type              - default - Description
     * append    - boolean           - true    - Should we add to the old log or create a new one?
     * mode      - string or integer - 0644    - What permission's mode should we open the log in?
     * dirmode   - string or integer - 0755    - What permission mode should we open newly created directories in?
     * autoflush - boolean           - true    - Should we flush after every logging statement?  This is great for getting
     *     logs of crashes, but may incur a performance hit.
     * schema    - filename          - null    - What schema should we validate the log file against?
     *     If null then we do not validate.
     *
     * @param string $filename Name of the log file.
     * @param string $ident    The identity string.
     * @param array  $conf     The configuration array.
     * @param int    $level    Log messages up to and including this level.
     *
     * @throws Log_XML_Exception Throws an exception for a badly formed filename.
     */
    public function __construct($filename = Log_XML::DEFAULT_FILENAME,
        $ident    = '',
        $conf     = array(),
        $level= PEAR_LOG_DEBUG
    ) {
        $this->_id         = md5(microtime());
        $this->_ident      = $ident;
        $this->_mask       = Log::UPTO($level);
        $this->fileToolkit = new Log_XML_FileToolkit();
        $this->filename    = $this->fileToolkit->getRealPath($filename);
        $this->_opened     = false;

            // if directory then append default filename
        if (is_dir($this->filename)) {
            $this->filename .= Log_XML::DEFAULT_FILENAME;
        }

        if ($this->filename === null) {
            //null for bad filenames
            throw new Log_XML_Exception("Invalid filename passed " .
                    "to Log_XML constructor: $filename");
        }


        if (isset($conf['append'])) {
            $this->append = $conf['append'];
        }

        if (!empty($conf['mode'])) {
            if (is_string($conf['mode'])) {
                $this->mode = octdec($conf['mode']);
            } else {
                $this->mode = $conf['mode'];
            }
        }

        if (!empty($conf['dirmode'])) {
            if (is_string($conf['dirmode'])) {
                $this->dirmode = octdec($conf['dirmode']);
            } else {
                $this->dirmode = $conf['dirmode'];
            }
        }

        if (isset($conf['autoflush'])) {
            $this->autoflush = $conf['autoflush'];
        }
    }

    /**
     * Destructor
     */
    public function __destruct()
    {
        if ($this->_opened) {
            $this->close();
        }
    }

    /**
     * Opens the log file for output.  If the specified log file does not
     * already exist, it will be created.  By default, new log entries are
     * appended to the end of the log file.
     *
     * This is implicitly called by log(), if necessary.
     *
     * @return bool True on success and false on failure
     */
    public function open()
    {
        // Does direct file access

        // no longer deleted
        $this->deleted = false;

            // If opened, then nothing to do
        if ($this->_opened === true) {
            return true;
        }

        $this->_opened = false;

            // If the log file's directory doesn't exist, create it.
        if (!is_dir(dirname($this->filename))) {
            $this->fileToolkit->mkpath($this->filename, $this->dirmode);
        }

            // Determine whether the log file needs to be created.
        $creating = !file_exists($this->filename);

        if ($creating) {
            try {
                $ret = touch($this->filename);
                if (!$ret) {
                    return false;
                }
            } catch (exception $e) {
                return false;
            }
        }

        $this->_opened = true;
        return $this->_opened;
    }


    /**
     * Closes the log file if it is open.
     *
     * @return bool True on success and false on failure.
     */
    public function close()
    {
        // If the log file is open, close it.
        if (!$this->_opened) {
            return ($this->_opened === false);
        }

        if ($this->autoflush) {
            $this->flush();
        }
        $this->_opened     = false;

        return ($this->_opened === false);
    }

    /**
     * Logs $message to the xml file.  The message is also passed along
     * to any Log_observer instances that are observing this Log.
     *
     * @param mixed  $message  String or object containing the message to log.
     * @param string $priority The priority of the message.  Valid
     *                  values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT,
     *                  PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING,
     *                  PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG.
     *
     * @return boolean  True on success or false on failure.
     */
    public function log($message = "", $priority = null)
    {
        if ($this->deleted === true) {
            return false;
        }

            // If a priority hasn't been specified, use the default value.
        if ($priority === null) {
            $priority = $this->_priority;
        }

            // Abort early if the priority is above the maximum logging level.
        if (!$this->_isMasked($priority)) {
            return false;
        }

            // If the log file isn't already open, open it now.
        if (!$this->_opened && !$this->open()) {
            return false;
        }

            // Extract the string representation of the message.
        $message = $this->_extractMessage($message);

            // Write the log entry to the log file.
        $ret = $this->createLogEntry($message);

            // Notify observers about this log message.
        $this->_announce(
            array('priority' => $priority,
                    'message' => $message)
        );

        if ($this->autoflush) {
            $this->flush();
        }
        return $ret;
    }


    /**
     * Alias of flush()
     *
     * @return bool True on success and false on failure.
     */
    public function save()
    {
        return $this->flush();
    }

    /**
     * Saves as a new filename.
     *
     * @param string $filename the filename to save to
     *
     * @return boolean true on success, false on failure
     */
    public function saveAs($filename)
    {
        $absoluteFilename = $this->fileToolkit->getRealPath($filename);
        if ($absoluteFilename === null) {
            return false;
        }
        $wasOpen = $this->_opened;
        $this->close();
        $this->filename = $absoluteFilename;
        $ret            = $this->open();
        if (!$wasOpen) {
            // if it was closed
            $this->close(); // preserve state
        }
        return $ret;
    }

    /**
     * Immediately deletes and closes the log.
     *
     * @return boolean true on success, false on failure
     */
    public function delete()
    {
        $this->deleted = true;
        if ($this->_opened === true) {
            $this->close();
        }
        if (is_file($this->filename)) {
            return unlink($this->filename);
        }
        return true;
    }

    /**
     * Flushes all pending data to the file handle.
     *
     * @since Log 1.8.2
     * @return boolean true on success, false on failure
     */
    public function flush()
    {
        // If not open, return false
        if ($this->_opened === false
            || $this->deleted
        ) {
            return false;
        }
        return true;
    }


    /**
     * Returns the base filename of the log, including suffix (ie, .xml).
     *
     * @return string name of the file
     */
    public function getFilename()
    {
        return basename($this->filename);
    }


    /**
     * Returns the pull path to the log file.
     *
     * @return string full path of the file
     */
    public function getAbsoluteFilename()
    {
        return $this->filename;
    }

    /**
     * This is an alias of getAbsoluteFileName().
     *
     * @return the absolute path of the filename as a string.
     */
    public function getFullFilename()
    {
        return $this->getAbsoluteFilename();
    }

    /* PROTECTED FUNCTIONS */


    /**
     * Add the header to the xml file.
     *
     * @return boolean true on success, false on failure
     */
    abstract protected function createHeader();


    /**
     * Creates the log entry in the xml file.
     *
     * @param mixed $message the message to log
     *
     * @return boolean true on success, false on failure
     */
    abstract protected function createLogEntry($message);

    /**
     * The default filename
     * @var string
     */
    const DEFAULT_FILENAME = 'php_log.xml';

    /**
     * String containing the name of the log file.
     * @var string
     */
    protected $filename;

    /**
     * If deleted we're in limbo until out of scope
     * If deleted then _autodelete should be true
     * @var boolean
     */
    protected $deleted = false;

    /**
     * Should new log entries be append to an existing log file, or should the
     * a new log file overwrite an existing one?
     * @var boolean
     */
    protected $append = true;

    /**
     * Integer (in octal) containing the log file's permissions mode.
     * @var integer
     */
    protected $mode = 0644;

    /**
     * Integer (in octal) specifying the file permission mode that will be
     * used when creating directories that do not already exist.
     * @var integer
     */
    protected $dirmode = 0755;

    /**
     * Determines weather flush is called implicitly with every log() call or
     * if flush must be called explicitly.
     *
     * @var boolean
     */
    protected $autoflush = true;

    /**
     * A wrapper class for file functions.
     *
     * @var LogXMLFileToolkit
     */
    protected $fileToolkit = null;
}
